﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Configuration;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
using System.Linq;

namespace TeamCityConfigurationMonitor.Core
{
	public class Monitor
	{
		private const string TeamCityDataFolderSettingKey = "TeamCityDataFolder";
		private const string TfsUrlSettingKey = "TeamFoundationServerUrl";
		private const string ServerPathSettingKey = "ServerPath";
		private static readonly string WorkspacePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), WorkspaceName);

		private readonly string configurationLocation;
		private readonly Uri tfsUrl;
		private readonly string serverPath;
		private FileSystemWatcher watcher;
		private Timer timer;
		private Workspace workspace;
		private TfsTeamProjectCollection teamProjectCollection;
		private VersionControlServer vcs;
		private readonly int changeNotificationTimeoutInSeconds;
		private readonly object synchronizationLock = new object();
		private readonly IList<FileSystemChange> changes = new List<FileSystemChange>();
		private readonly object changesLock = new object();
		private const string UsernameSettingKey = "Username";
		private const string PasswordSettingKey = "Password";
		private readonly string username;
		private readonly string password;
		private const string FilesToMonitor = "*.xml";
		private const string ChangeNotificationTimeoutSettingKey = "ChangeNotificationTimeoutInSeconds";
		private const string WorkspaceName = "TeamCityConfigurationMonitor";

		public Monitor()
		{
			configurationLocation = LoadConfigurationLocation();
			tfsUrl = LoadTfsUrl();
			serverPath = LoadServerPath();
			changeNotificationTimeoutInSeconds = LoadChangeNotificationIdleTimeout();
			username = GetOptionalSetting<string>(UsernameSettingKey);
			password = GetOptionalSetting<string>(PasswordSettingKey);
		}

		private static int LoadChangeNotificationIdleTimeout()
		{
			return GetOptionalSetting(ChangeNotificationTimeoutSettingKey, 10);
		}

		private static string LoadServerPath()
		{
			var path = GetMandatorySetting<string>(ServerPathSettingKey);

			if(!path.StartsWith("$/"))
				throw new InvalidOperationException(string.Format("{0} must start with $/", ServerPathSettingKey));

			return path;
		}

		public void Start()
		{
			CreateTeamProjectCollection();
            
			vcs = teamProjectCollection.GetService<VersionControlServer>();

			if(!IsConfigurationLocationValid())
			{
				throw new InvalidOperationException("Configuration folder does not exists or is empty");
			}

			if(!IsServerPathValid())
			{
				throw new InvalidOperationException(string.Format("{0} must point to a valid folder on the server", ServerPathSettingKey));
			}

			workspace = WorkspaceExists() ? GetWorkspace() : CreateWorkspace();

			if (ThereArePendingChanges())
				UndoPendingChanges();

			CleanupWorkspace();

			GetLatestVersion();

			Synchronize();

			StartWatching();
		}

		private void CreateTeamProjectCollection()
		{
			if(string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
				teamProjectCollection = new TfsTeamProjectCollection(tfsUrl);
			else
			{
				var credentials = GetCredentials();

				teamProjectCollection = new TfsTeamProjectCollection(tfsUrl, credentials);
			}

			teamProjectCollection.Authenticate();
		}

		private NetworkCredential GetCredentials()
		{
			if(string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
				throw new ConfigurationErrorsException(
					string.Format("If you want to authenticate with supplied credentials you need to put both {0} and {1} settings in the configuration file",
						UsernameSettingKey, PasswordSettingKey));

			var split = username.Split(new[]{@"\"}, StringSplitOptions.RemoveEmptyEntries);

			return split.Length > 1
			       	? new NetworkCredential(split[1], password, split[0])
			       	: new NetworkCredential(username, password);
		}

		private void StartWatching()
		{
			timer = new Timer(TimerElapsed, null, Timeout.Infinite, Timeout.Infinite);

			watcher = new FileSystemWatcher(configurationLocation, FilesToMonitor)
			          {
			          	IncludeSubdirectories = true,
			          	NotifyFilter = NotifyFilters.Attributes | NotifyFilters.CreationTime | NotifyFilters.DirectoryName |
			          	               NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Size
			          };

			watcher.Changed += CollectChangeAndShiftTimer;
			watcher.Created += CollectChangeAndShiftTimer;
			watcher.Deleted += CollectChangeAndShiftTimer;
			watcher.Renamed += CollectChangeAndShiftTimer;

			watcher.EnableRaisingEvents = true;
		}

		private void TimerElapsed(object state)
		{
			Synchronize();	
		}

		private void CollectChangeAndShiftTimer(object sender, FileSystemEventArgs e)
		{
			timer.Change(changeNotificationTimeoutInSeconds * 1000, Timeout.Infinite);

			lock(changesLock)
				changes.Add(new FileSystemChange(e.ChangeType, e.FullPath, e.Name));
		}

		private static void CleanupWorkspace()
		{
			DeleteDirectory(WorkspacePath);

			Directory.CreateDirectory(WorkspacePath);
		}

		private static void DeleteDirectory(string dir)
		{
			foreach (var file in Directory.GetFiles(dir))
				DeleteFile(file);

			foreach (var directory in Directory.GetDirectories(dir))
				DeleteDirectory(directory);

			Directory.Delete(dir);
		}

		private static void DeleteFile(string file)
		{
			File.SetAttributes(file, FileAttributes.Normal);
			File.Delete(file);
		}

		private bool IsConfigurationLocationValid()
		{
			return Directory.Exists(configurationLocation) && Directory.GetFileSystemEntries(configurationLocation).Any();
		}

		private void Synchronize()
		{
			if(!System.Threading.Monitor.TryEnter(synchronizationLock))
				return;

			try
			{
				var sourceFolder = configurationLocation;
				var targetFolder = WorkspacePath;

				var localChanges = GetLocalChanges();
				var sourceFiles = GetFilesSpec(sourceFolder, targetFolder);
				var destinationFiles = GetFilesSpec(targetFolder, sourceFolder);

				var filesToDelete = destinationFiles.Where(spec => !File.Exists(spec.TargetPath)).ToArray();
				var filesToAdd = sourceFiles.Where(spec => !File.Exists(spec.TargetPath)).ToArray();
				var filesToOverwrite = sourceFiles.Except(filesToAdd).ToArray();

				PerformDeletions(targetFolder, filesToDelete);

				PerformAdditions(filesToAdd);

				PerformOverwrites(filesToOverwrite);

				workspace.CheckIn(workspace.GetPendingChanges(), BuildCheckinComment(localChanges));
			}
			finally
			{
				System.Threading.Monitor.Exit(synchronizationLock);
			}
		}

		private void PerformOverwrites(IEnumerable<FileSpec> filesToOverwrite)
		{
			foreach (var spec in filesToOverwrite)
			{
				workspace.PendEdit(spec.TargetPath);
				File.Copy(spec.Path, spec.TargetPath, true);
			}
		}

		private void PerformAdditions(IEnumerable<FileSpec> filesToAdd)
		{
			foreach (var spec in filesToAdd)
			{
				var targetDirectory = Path.GetDirectoryName(spec.TargetPath);

				if (targetDirectory != null && !Directory.Exists(targetDirectory))
					Directory.CreateDirectory(targetDirectory);

				File.Copy(spec.Path, spec.TargetPath);
				workspace.PendAdd(spec.TargetPath);
			}
		}

		private void PerformDeletions(string targetFolder, IEnumerable<FileSpec> filesToDelete)
		{
			foreach (var spec in filesToDelete)
			{
				workspace.PendDelete(spec.Path);
			}

			foreach (var directory in EmptyDirectories(targetFolder))
			{
				workspace.PendDelete(directory);
			}
		}

		private IEnumerable<FileSystemChange> GetLocalChanges()
		{
			FileSystemChange[] localChanges;

			lock (changesLock)
			{
				localChanges = new FileSystemChange[changes.Count];
				changes.CopyTo(localChanges, 0);
				changes.Clear();
			}

			return localChanges;
		}

		private static string BuildCheckinComment(IEnumerable<FileSystemChange> fileSystemChanges)
		{
			var grouped = fileSystemChanges.GroupBy(change => change.ChangeType)
				.Select(g => new { ChangeType = g.Key.ToString(), NumberOfChanges = g.Count() });

			var result = new StringBuilder("TeamCity configuration monitor synchronization.");

			if (grouped.Any())
				result.AppendLine();

			foreach (var g in grouped)
				result.AppendLine().AppendFormat("{0}: {1}", g.ChangeType, g.NumberOfChanges);

			return result.ToString();
		}

		private static IEnumerable<string> EmptyDirectories(string targetFolder)
		{
			return Directory.GetDirectories(targetFolder, "*.*", SearchOption.AllDirectories).Where(IsEmpty);
		}

		private static bool IsEmpty(string directory)
		{
			return Directory.GetFileSystemEntries(directory).Count() == 0;
		}

		private static IEnumerable<FileSpec> GetFilesSpec(string sourceFolder, string targetFolder)
		{
			return Directory.GetFiles(sourceFolder, FilesToMonitor, SearchOption.AllDirectories)
				.Select(f => new
				             {
				             	Path = f,
				             	RelativePath = f.Substring(sourceFolder.Length).TrimStart('\\')
				             })
				.Select(f => new FileSpec
				             {
				             	Path = f.Path,
				             	TargetPath = Path.Combine(targetFolder, f.RelativePath)
				             })
				// important! greedy
				.ToArray();
		}


		private void GetLatestVersion()
		{
			workspace.Get(VersionSpec.Latest, GetOptions.GetAll | GetOptions.Overwrite);
		}

		private void UndoPendingChanges()
		{
			workspace.Undo(workspace.GetPendingChanges().Select(p => p.LocalItem).ToArray());
		}

		private Workspace GetWorkspace()
		{
			return GetLocalWorkspaceInfo().GetWorkspace(vcs.TeamProjectCollection);
		}

		private bool ThereArePendingChanges()
		{
			return workspace.GetPendingChangesEnumerable().Any();
		}

		private bool IsServerPathValid()
		{
			var item = vcs.GetItem(serverPath);

			return item != null && item.ItemType == ItemType.Folder;
		}

		private Workspace CreateWorkspace()
		{
			return vcs.CreateWorkspace(WorkspaceName, vcs.AuthorizedUser, string.Format("Workspace created by {0}", WorkspaceName),
			                    new[] {new WorkingFolder(serverPath, WorkspacePath)});
		}

		private bool WorkspaceExists()
		{
			return GetLocalWorkspaceInfo() != null;
		}

		private WorkspaceInfo GetLocalWorkspaceInfo()
		{
			return Workstation.Current.GetLocalWorkspaceInfo(vcs, WorkspaceName, vcs.AuthorizedUser);
		}

		private static Uri LoadTfsUrl()
		{
			return GetMandatorySetting<Uri>(TfsUrlSettingKey);
		}

		private static string LoadConfigurationLocation()
		{
			var dataFolder = GetMandatorySetting<string>(TeamCityDataFolderSettingKey);

			if (!Path.IsPathRooted(dataFolder))
				throw new ConfigurationErrorsException(string.Format("{0} setting should be an absolute path", TeamCityDataFolderSettingKey));
			
			return Path.Combine(dataFolder, "config");
		}

		private static bool TryGetSetting(string settingKey, out string value)
		{
			value = Settings[settingKey];
			return !string.IsNullOrEmpty(value);
		}

		private static T GetMandatorySetting<T>(string settingKey)
		{
			string value;
			var got = TryGetSetting(settingKey, out value);

			if(!got)
				throw new ConfigurationErrorsException(string.Format("{0} setting is mandatory", settingKey));

			return Convert<T>(value);
		}

		private static T GetOptionalSetting<T>(string settingKey)
		{
			return GetOptionalSetting(settingKey, default(T));
		}

		private static T GetOptionalSetting<T>(string settingKey, T @default)
		{
			string value;
			var got = TryGetSetting(settingKey, out value);

			return got ? Convert<T>(value) : @default;
		}

		private static T Convert<T>(string value)
		{
			var typeConverter = TypeDescriptor.GetConverter(typeof (T));

			if (typeConverter != null) 
				return (T) typeConverter.ConvertFromString(value);

			return (T) System.Convert.ChangeType(value, typeof (T));
		}

		private static NameValueCollection Settings
		{
			get { return ConfigurationManager.AppSettings; }
		}

		public void Stop()
		{
			if(watcher != null)
				watcher.Dispose();

			if (timer != null)
				timer.Dispose();

			if(teamProjectCollection != null)
				teamProjectCollection.Dispose();
		}
	}
}